<!DOCTYPE html>
<title>ScrollTimelines may trigger multiple style/layout passes</title>
<link rel="help" src="https://github.com/w3c/csswg-drafts/issues/5261">
<link rel="help" src="https://drafts.csswg.org/scroll-animations-1/#avoiding-cycles">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="/web-animations/testcommon.js"></script>
<style>
  @keyframes expand_width {
    from { width: 100px; }
    to { width: 100px; }
  }
  @keyframes expand_height {
    from { height: 100px; }
    to { height: 100px; }
  }
  @scroll-timeline timeline1 {
    source: selector(#scroller1);
    time-range: 10s;
  }
  @scroll-timeline timeline2 {
    source: selector(#scroller2);
    time-range: 10s;
  }
  main {
    height: 0px;
    overflow: hidden;
  }
  .scroller {
    height: 100px;
    overflow: scroll;
  }
  .scroller > div {
    height: 200px;
  }
  #element1 {
    width: 1px;
    animation: expand_width 10s timeline1;
  }
  #element2 {
    height: 1px;
    animation: expand_height 10s timeline2;
  }
</style>
<main id=main></main>
<div id=element1></div>
<div>
  <div id=element2></div>
</div>
<script>
    function insertScroller(id) {
      let scroller = document.createElement('div');
      scroller.setAttribute('id', id);
      scroller.setAttribute('class', 'scroller');
      scroller.append(document.createElement('div'));
      main.append(scroller);
    }

    promise_test(async () => {
      await waitForNextFrame();

      let events1 = [];
      let events2 = [];

      insertScroller('scroller1');
      // Even though #scroller1 was just inserted into the DOM, |timeline1|
      // remains inactive until the next frame.
      //
      // https://drafts.csswg.org/scroll-animations-1/#avoiding-cycles
      assert_equals(getComputedStyle(element1).width, '1px');
      (new ResizeObserver(entries => {
        events1.push(entries);
        insertScroller('scroller2');
        assert_equals(getComputedStyle(element2).height, '1px');
      })).observe(element1);

      (new ResizeObserver(entries => {
        events2.push(entries);
      })).observe(element2);

      await waitForNextFrame();

      // According to the basic rules of the spec [1], the timeline is
      // inactive at the time the resize observer event was delivered, because
      // #scroller1 did not have a layout box at the time style recalc for
      // #element1 happened.
      //
      // However, an additional style/layout pass should take place
      // (before resize observer deliveries) if we detect new ScrollTimelines
      // in this situation, hence we ultimately do expect the animation to
      // apply [2].
      //
      // [1] https://drafts.csswg.org/scroll-animations-1/#avoiding-cycles
      // [2] https://github.com/w3c/csswg-drafts/issues/5261
      assert_equals(events1.length, 1);
      assert_equals(events1[0].length, 1);
      assert_equals(events1[0][0].contentBoxSize.length, 1);
      assert_equals(events1[0][0].contentBoxSize[0].inlineSize, 100);

      // ScrollTimelines created during the ResizeObserver should remain
      // inactive during the frame they're created, so the ResizeObserver
      // event should not reflect the animated value.
      assert_equals(events2.length, 1);
      assert_equals(events2[0].length, 1);
      assert_equals(events2[0][0].contentBoxSize.length, 1);
      assert_equals(events2[0][0].contentBoxSize[0].blockSize, 1);

      assert_equals(getComputedStyle(element1).width, '100px');
      assert_equals(getComputedStyle(element2).height, '100px');
    }, 'Multiple style/layout passes occur when necessary');
</script>
